Skip to content

perf(locator): stop searching once the caller's matches are found - #5

Open
caipira113 wants to merge 1 commit into
andelf:mainfrom
caipiralink:fix/locator-early-exit
Open

perf(locator): stop searching once the caller's matches are found#5
caipira113 wants to merge 1 commit into
andelf:mainfrom
caipiralink:fix/locator-early-exit

Conversation

@caipira113

Copy link
Copy Markdown

Problem

Locator resolution collects every match before discarding all but the ones the caller asked for. resolve() calls resolve_all() and takes the first element; nth=N and first are applied as selection steps, after the search that produced the candidates has already finished walking the tree.

On a small tree that is invisible. On an application that materialises accessibility children on demand it dominates runtime, because the walk keeps asking the app to build elements long after the answer is known. Microsoft Word with a 74k-character document open, where AXLayoutArea reports kids=0 until something descends into it:

$ axcli --pid 704 snapshot --depth 4 --simplify
   ... 139.55s

The same traversal primitives are fine when they stop early — a depth-capped walk of the whole app costs 0.07s — so this is the search continuing past the point of usefulness rather than the AX tree being inherently slow.

It also makes failure slower than success: proving a locator ambiguous walked the entire tree to produce an exact count, so the error path cost more than the path that found an element.

Change

Each pipeline step now receives the number of candidates the steps after it can actually consume, computed backwards from what the caller requested.

Following step Candidates the previous step must produce
nth=N (N ≥ 0) N + 1
first 1
last all — the end is only known once everything is collected
nth=-N all — a negative index counts from the end
another search step all — each candidate is a separate search root

Callers supply the final budget: resolve_one asks for two (one to act on, one to prove ambiguity), snapshot without --all asks for two (one to print, one to report that more exist), and locate_all asks for usize::MAX, which reproduces the previous behaviour exactly.

Three bounded variants were added alongside the existing functions rather than replacing them — find_limited, collect_matching_limited, resolve_locator_limited — each equal to its unbounded counterpart at usize::MAX.

Verification

Tested on macOS 27.0 (build 26A5388g, arm64), Word 16.111.3.

Word — a large document open, the pathological case

Command Before After
snapshot --depth 4 --simplify 139.55s 0.14s
snapshot splitgroup --depth 1 94.43s 0.11s
get role splitgroup >200s (timeout) 1.98s

Finder — output equivalence

Ten locators covering each budget rule, comparing the old binary against the new one:

Finder: 9/10 byte-identical (stdout + stderr)

The tenth is the intended difference described below; its selected element and printed tree are identical.

cargo test passes (62 tests, 7 new) and cargo clippy reports 47 warnings both before and after, so no new ones.

One deliberate behaviour change

The ambiguity message now reports a lower bound:

- locator matched 8 elements, must be unique for actions
+ locator matched at least 2 elements, must be unique for actions

An exact total requires the full traversal this change exists to avoid, and it is a number the caller cannot act on — the suggested fix is >> nth=N either way. --all still reports the true count, because it genuinely collects every match.

Not addressed

A locator matching exactly one element is still slow on Word: proving there is no second match requires exhausting the tree. That is a traversal-order problem — DFS descends into the document subtree before reaching siblings such as the menu bar — and changing the order would change which element nth=N selects, so it is left out of this PR.

Resolution collected every match before discarding all but the ones the
caller asked for. On an application that materialises accessibility
children on demand this dominates runtime: with a large document open,
`snapshot --depth 4` on Word took 139s because the walk continued through
the document canvas long after the answer was known.

Each step now receives the number of candidates the following steps can
actually consume, computed backwards from the caller's request. `nth=N`
needs N+1, `first` needs one, and `last`, a negative `nth`, or a following
search step still need the full set, so those keep walking. `resolve_one`
asks for two: one to act on, one to prove ambiguity.

Measured on Word with a 74k-character document open:

  snapshot --depth 4          139.55s -> 0.14s
  snapshot splitgroup          94.43s -> 0.11s
  get role splitgroup         >200s   -> 1.98s

Results are unchanged. The one visible difference is the ambiguity error,
which now reports "at least N" rather than a total: proving ambiguity no
longer requires counting every match, and paying a full traversal for a
number the caller cannot act on is what made the error slower to produce
than the successful path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant